Juan M. Fonseca-Solís · Agosto 2020 · 5 min read
Los estetoscopios digitales son herramientas que le permiten a los médicos realizar remotamente un seguimiento de sus pacientes por problemas de salud relacionados, por ejemplo, con el corazón y los pulmones. En muchos de los casos, estos estetoscopios digitales utilizan los mismos codecs que son empleados en sistemas de teleconferencia como Zoom, MS Teams y Skype. El éxito de estos codecs está en que son capaces de transmitir voz y música de alta calidad empleando bajas tasas de bits; para ello se valen de que el oído humano es incapaz de escuchar ciertas frecuencias y por eso las eliminan. Aunque esto resulte excelente para cuestiones de recreación y comunicación, no es tan bueno para los algoritmos de reconocimiento de patrones, que necesitan la información completa para sus algoritmos. En este ipython notebook nos dimos a la tarea de realizar pruebas al códec OPUS para determinar si la pérdida de la calidad es realmente significativa. Los resultados mostraron que, para un algoritmos de reconocimiento de patrones robusto y aplicando un filtrado pasabajas y una tasa de bits de 8kbps, el algoritmo OPUS parace ser apropiado para transmitir sonidos producidos por el corazón y los pulmones sin necesidad de transmitir datos crudos.
En comparación a otros algoritmos como el MP3, AAC y Vorbis, el códec OPUS ofrece una calidad de audio similar pero a una menor tasa de bits y es capaz de cubrir casi todo el rango de transmisión (ver figura 1). Esto significa que se puede ofrecer una calidad de audio mejor al mismo costo. Opus está compuesto por dos algoritmos llamados SILK (creado por Skype Limited para comprimir voz) y Constrained Energy Lapped Transform (CELT) (para comprimir música) [3, 4, 13]; así como un modo híbrido.
Figura 1. Tomada de https://opus-codec.org/static/comparison/quality.svg.
La mayoría de códecs de sonido utilizan la teoría psicoacústica para "engañar" al oído humano (más exáctamente, la cóclea) haciendo que perciba la misma calidad de audio a costa de eliminar las frecuencias inaudibles. Para ello, utilizan conceptos tales como las escalas Bark y ERB, bandas críticas, enmascaramiento frecuencial, enmascaramiento temporal y predicción lineal (LPC, por sus siglas en Inglés). A continuación hacemos un resumen de estos conceptos:
Figura 2. Tomada de https://en.wikipedia.org/wiki/Auditory_system#/media/File:Anatomy_of_the_Human_Ear.svg.
%pylab inline
import numpy as np
def erb(F):
return 21.4 * np.log10(0.00437*F+1)
F = np.linspace(0,20000,1000) # 0 a 20k Hz es el rango de audición humana
E = erb(F)
pylab.plot(F/1e3,E)
pylab.ylabel('Frequency (kHz)')
pylab.xlabel('ERB (kHz)')
pylab.show()
E = np.linspace(0,erb(20000),32) # 32 puntos equidistantes
F = (np.power(10,E/21.4)-1)/0.00437 # invertimos el logaritmo de la escala ERB
print(F.astype(int))
pylab.stem(F,np.ones(len(F)))
pylab.xlabel('Frecuencia central de cada subfiltro (Hz)')
pylab.show()
Enmascaramiento frecuencial: fenómeno producido las bandas críticas (ver figura 3) donde la frecuencia de mayor energía enmascara a las de menor energía usando su envolvente espectral.
Figura 3. Tomada de https://output.com/blog/9-sound-design-tips-to-hack-your-listeners-ears.
Enmascaramiento temporal: fenómeno que ocurre cuando dos eventos ocurren lo suficientemente cercanos en el tiempo y el más fuerte enmascará al más débil si el segundo ocurre 10 ms antes (pre-enmascaramiento) o 30-60 ms después (post-enmascaramiento) que el primero [7] (ver figura 4).
Figura 4. Tomada de https://d3i71xaburhd42.cloudfront.net/13674722d0e4fc8a6877773b25a62ee9850eb46a/3-Figure2-1.png.
Basándonos en los conceptos de psicoacústica explicados arriba, y considerando que el rango de frecuencias para los sonidos producidos por los pulmones es de $[60,1000]$ Hz y del corazón es de $[20, 500]$ Hz, podemos definir una serie de casos de prueba para determinar si la pérdida de información frecuencial es significativa [14, 15].
Para probar el caso 1 se puede utilizar un barrido de frecuencias que siga la siguiente ecuación [6] $$ F(t) = \Big(\frac{F_1-F_0}{T}\Big)t + F_0, $$
El rango de frecuencias a usar es de $[20, 20000]$ Hz (el rango de audición humana). El barrido puede ser construido usando un senoidal de la forma $x[t] = A \sin(2\pi Ft)$.
%pylab inline
from scipy.io import wavfile
from IPython.display import Audio
import numpy as np
def plotSpecgram(x,Fs,fMax=None):
# spectrogram
fig, ax = pylab.subplots(nrows=1)
ax.specgram(x, NFFT=1024, Fs=Fs, noverlap=900)
pylab.xlabel('Tiempo (s)')
pylab.ylabel('Frecuencia (Hz)')
if fMax != None:
pylab.ylim([0,fMax])
pylab.show()
def plotFFT(x,Fs,xlim=None):
pylab.figure()
N2=int(len(x)/2)
F = np.linspace(0,Fs/2,N2)/1e3
X = np.sqrt(np.abs(np.fft.fft(x)[0:N2]))
pylab.plot(F, X)
X[0] = 0 # remove DC value
pylab.xlabel('Frecuencia (kHz)')
pylab.ylabel('$\sqrt{|S(f)|}$')
if xlim != None:
pylab.xlim(xlim)
pylab.show()
rango = [20.0, 20000.0] # el rango promedio de audicion humano en Hz
Fs = 44.1 * 1e3 # la tasa de muestreo de los equipos comerciales
T = 1.0 # segundos (t1-t0)
N = int(T*Fs)
n = np.arange(0,N)
F0 = (rango[1]-rango[0])*n/N + rango[0]
x = np.sin(np.pi*F0/Fs*n) # f=F0/Fs: frecuencia discreta
plotSpecgram(x,Fs)
plotFFT(x,Fs)
wavfile.write('/tmp/barrido.wav',int(Fs),x.astype(np.float32))
Audio(x, rate=Fs)
El espectrograma muestra que el barrido va de los 20 a los 20000 Hz como se deseaba. Asimismo, la magnitud espectral muestra una recta horizontal casi en todo el rango, indicando que todas las frecuencias están presentes como se supone que tiene que pasar.
Para el caso de prueba 2, podemos construir una señal de dos tonos como sigue:
y = np.sin(np.pi*590/(Fs/2)*n) + np.sin(np.pi*610/(Fs/2)*n)
plotSpecgram(y,Fs,fMax=2000)
plotFFT(y,Fs,xlim=[500/1e3,700/1e3])
wavfile.write('/tmp/enmascaramiento.wav',int(Fs),x.astype(np.float32))
Audio(y, rate=Fs)
def readPlayVisualizeFile(inputFile,fMax=None):
fs, x = wavfile.read(inputFile)
y = np.array(x)/max(x)
if None==fMax:
plotSpecgram(x,fs)
else:
plotSpecgram(x,fs,fMax)
return fs, x
#!sudo apt-get install ffmpeg
#!sudo apt-get install opus-tools
!ffmpeg -loglevel error -y -i /tmp/barrido.wav -qscale 0 /tmp/wavRaw.wav # opus-tools
!opusenc --quiet --bitrate 8 /tmp/wavRaw.wav /tmp/opusEnc.opus
!opusdec --quiet /tmp/opusEnc.opus /tmp/opusDec.wav
Fs, x = readPlayVisualizeFile('/tmp/opusDec.wav')
plotFFT(x,Fs)
Audio(x, rate=Fs)
El espectrograma muestra que partir de los casi 5 kHz el barrido de frecuencia se pliega, en un efecto llamado aliasing, lo que produce que veamos frecuencias que en realidad no existen, esto sí que podría afectar la calidad del clasificador. Para resolverlo se podría emplear un filtro pasabajas con frecuencia de corte 5 kHz antes de realizar la compresión. Por su parte, la magnitud espectral muestra que a partir de los 5 kHz las frecuencias altas son atenuadas. Probemos ahora con 128 kbps (fullband stereo) a ver si la calidad mejora. Sin embargo, aunque se introdujeron artefactos que antes no estaban, el rango $[20,1000]$ Hz sigue estando casi intacto, lo cual indica que para un algoritmo de reconocimiento de patrones lo suficientemente selectivo, la corrupción en la señal no debería afecta (hay que considerar si existen armónicas para asegurar esto completamente).
!ffmpeg -loglevel error -y -i /tmp/barrido.wav -qscale 0 /tmp/wavRaw.wav # opus-tools
!opusenc --quiet --bitrate 128 /tmp/wavRaw.wav /tmp/opusEnc.opus
!opusdec --quiet /tmp/opusEnc.opus /tmp/opusDec.wav
Fs, x = readPlayVisualizeFile('/tmp/opusDec.wav')
plotFFT(x,Fs)
Audio(x, rate=Fs)
El espectrograma sigue mostrando que hay cierto aliasing pero con bastante menor energía, lo cual favorable; y la magnitud espectral muestra una recta horizontal similar a la que vimos con la señal cruda. Por lo que podemos decir que con full-band ofrece una menor distorsión. En este punto valdría la pena preguntarse cuál de los tres métodos de OPUS realizó la compresión y si los resultados variarían en caso de que se fuerce algún método en particular (SILK, CELT o el híbrido).
Lorem ipsum...
!ffmpeg -loglevel error -y -i /tmp/enmascaramiento.wav -qscale 0 /tmp/wavRaw.wav # opus-tools
!opusenc --quiet --bitrate 8 /tmp/wavRaw.wav /tmp/opusEnc.opus
!opusdec --quiet /tmp/opusEnc.opus /tmp/opusDec.wav
Fs, x = readPlayVisualizeFile('/tmp/opusDec.wav')
plotFFT(x,Fs)
Audio(x, rate=Fs)
Nota: a partir de este momento se recomienda usar audifonos para apreciar mejor la calidad el audio.
Procedemos ahora a codificar y decodificar una grabación real y cruda usando OPUS para una tasa de bits de 8 Kbps (la misma empleada en una videollamada entre dos personas) [12]. Procedimos a tomar tomar una grabación del sitio https://www.kaggle.com/vbookshelf/respiratory-sound-database, la cual corresponde a una grabación de tomada de un individuo .
Fs, x = readPlayVisualizeFile('./wav/107_3p2_Tc_mc_AKGC417L_2.wav')
plotFFT(x,Fs)
Audio(x, rate=Fs)
Observamos que hay energía alta en el rango $[0,1.5]$ y otra menor en $[7.5,9.5]$ kHz. La primera parece corresponder a los sonidos naturales del cuerpo, la segundo para más bien una frecuencia parásita que podría haberse colado al momento de realizar la grabación (el cuerpo humano no produce tonos constantes y puros como los que se observan después de los 7500 Hz).
# OPUS
!ffmpeg -loglevel error -y -i ./wav/107_3p2_Tc_mc_AKGC417L_2.wav -qscale 0 /tmp/wavRaw.wav # same quality
!opusenc --quiet --bitrate 8 /tmp/wavRaw.wav /tmp/opusEnc.opus
!opusdec --quiet /tmp/opusEnc.opus /tmp/opusDec.wav
Fs, x = readPlayVisualizeFile('/tmp/opusDec.wav')
plotFFT(x,Fs)
Audio(x, rate=Fs)
El espectrograma muestra que la energía un poco antes de los 5 kHz fue atenuada, pero ya descubrimos que esto no importa tanto pues se sale del rango objetivo en los $[20,1000]$ Hz. Lo más importante parece ser que las frecuencias están inalterada en el rango deseado. La magnitud espectral muestra un espectro limpio.

Este obra está bajo una licencia de Creative Commons Reconocimiento-NoComercial-SinObraDerivada 4.0 Internacional. El sitio juanfonsecasolis.github.io es un blog dedicado a la investigación independiente en temas relacionados al procesamiento digital de señales. Para reutilizar este artículo y citar las fuente por favor utilice el siguiente Bibtex:
@online{Fonseca2020,
author = {Juan M. Fonseca-Solís},
title = { Pruebas por pares o pairwise testing},
year = 2020,
url = {https://juanfonsecasolis.github.io/blog/JFonseca.pairwisetesting.html},
urldate = {}
}